﻿
$.ajaxSetup({
    type: "POST",
    contentType: "application/json",
    dataType: "text json"
});

var spa = spa || {};

spa.dataContext = function (controllerName, itemConstructor) {
    /// <summary>
    /// Logic Used to Talk to a Controller
    /// </summary>
    /// <param name="controllerName" type="String">
    /// Required: The Name of the WebApi Controller
    /// </param>
    /// <param name="itemConstructor" type="function">
    /// Required: The function used to create each item
    /// </param>
    /// <returns>
    /// A dataContext Object
    /// </returns>
    var self = this;
    self.controllerUrl = "/api/" + controllerName;
    self.entityConstructor = itemConstructor;
    self.entities = [];

    self.attach = function (item) {
        if (self.entities.indexOf(item) < 0) {
            self.entities.push(item);
        }
    };
    self.detach = function (item) {
        self.entities.splice(self.entities.indexOf(item), 1);
    };

    self.loadOne = function (id, callBack) {
        /// <summary>
        /// Load a Single Item from the Controller
        /// </summary>
        /// <param name="id" type="Int">
        /// Required: The Id of the Item to Load
        /// </param>
        /// <param name="callBack" type="function">
        /// Required: CallBack that returns 1 Entity
        /// </param>
        $.ajax({
            url: self.controllerUrl,
            type: "GET",
            statusCode: {
                200: function (data) {
                    var item = new self.entityConstructor(data);
                    self.attach(item);
                    callBack(item);
                },
            }
        });
    };

    self.saveItem = function (item, callBack) {
        /// <summary>
        /// Save a Single Item to the Controller
        /// </summary>
        /// <param name="item" type="itemModel">
        /// The Item to Save (includes processing delete operation)
        /// </param>
        /// <param name="callBack" type="function">
        /// CallBack for post-processing (returns the entity)
        /// &#10; Occurs after the object has been updated
        /// </param>
        var state = item.EntityState();
        if (state == spa.entityStates.modified) {
            $.ajax({
                url: self.controllerUrl + "/" + item.Key(),
                type: "PUT",
                data: item.JsonData(),
                statusCode: {
                    200: function (data) {
                        item.DataRefresh(data);
                        if (callBack) {
                            callBack(item);
                        }
                    },
                }
            });
        }
        else if (state == spa.entityStates.added) {
            $.ajax({
                url: self.controllerUrl,
                type: "POST",
                data: item.JsonData(),
                statusCode: {
                    201: function (data) {
                        item.DataRefresh(data);
                        if (callBack) {
                            callBack(item);
                        }
                    },
                }
            });
        }
        else if (state == spa.entityStates.removed) {
            $.ajax({
                url: self.controllerUrl + "/" + item.Key(),
                type: "DELETE",
                statusCode: {
                    200: function (data) {
                        item.DataRefresh(data);
                        if (callBack) {
                            callBack(item);
                        }
                    },
                    404: function () {
                        alert('error');
                    },
                }
            });
        }
        else if (state == spa.entityStates.junk) {
            if (callBack) {
                callBack(item);
            }
        }
    };

    self.saveAll = function () {
        /// <summary>
        /// Save all attached Entities to the Controller
        /// </summary>
        $.map(self.entities, function (item, index) {
            self.saveItem(item, function (item) {
                if (item.EntityState() == spa.entityStates.junk) {
                    self.detach(item);
                }
            });
        });
    };
    
    self.loadAll = function (callBack) {
        /// <summary>
        /// Load a All Items from the Controller
        /// </summary>
        /// <param name="callBack" type="function">
        /// Required: CallBack that returns an Array of your Entities
        /// </param>
        $.ajax({
            url: self.controllerUrl,
            type: "GET",
            statusCode: {
                200: function (data) {
                    $.map(data, function (itemData) {
                        var item = new self.entityConstructor(itemData);
                        self.attach(item);
                    });
                    callBack(self.entities);
                },
            }
        });
    };
    
    return {
        LoadAll: self.loadAll,
        LoadOne: self.loadOne,
        SaveItem: self.saveItem,
        SaveAll: self.saveAll,
        Attach: self.attach,
        Detach: self.detach,
    };
};

spa.entityStates = {
    /// <summary>
    /// The States that a Self-Tracking Entity would Need
    /// <summary>
    unchanged: "unchanged", // Persisted, not changed
    added: "added", // Added, not saved
    removed: "removed", // Removed, not saved
    junk: "junk", // No Longer Valid in the Collection (should be removed from all Views)
    modified: "modified" // Changed, not saved
};


spa.itemModel = function (data, initialState) {
    /// <summary>
    /// Creates a Self-Tracking Item Model
    /// </summary>
    /// <param name="data" type="Javascript object">
    /// Required: Initial Data of the Object
    /// </param>
    /// <param name="initialState" type="function">
    /// Optional: one of spa.entityStates [Default: unchanged]
    /// </param>
    var self = this;
    self.origData = data;
    self.entity = ko.mapping.fromJS(data);
    self.entityState = ko.observable(initialState || spa.entityStates.unchanged);

    self.key = ko.computed(function () {
        // this needs to be adjusted base on your Entity
        return self.entity.ID();
    });

    self.dataRefresh = function (data) {
        /// <summary>
        /// Refreshes the Item and resets the entitystate to spa.entityStates.unchanged
        /// </summary>
        /// <param name="data" type="Javascript object">
        /// Required: data to reset the object to
        /// </param>
        self.origData = data;
        ko.mapping.fromJS(data, self.entity);
        self.entityState(spa.entityStates.unchanged);
    };

    self.getState = ko.computed(function () {
        /// <summary>
        /// Returns the Current Entity State (one of spa.entityStates)
        /// </summary>
        return self.entityState(); 
    });

    self.setModified = function () {
        /// <summary>
        /// Marks the Entity as spa.entityStates.modified (if currently spa.entityStates.unchanged)
        /// </summary>
        if (self.entityState() == spa.entityStates.unchanged) {
            self.entityState(spa.entityStates.modified);
        }
    };

    self.setUnchanged = function () {
        /// <summary>
        /// Marks the Entity as spa.entityStates.unchanged (if currently spa.entityStates.modified)
        /// </summary>
        if (self.entityState() == spa.entityStates.modified) {
            self.entityState(spa.entityStates.unchanged);
        }
    };

    self.markForDeletion = function () {
        /// <summary>
        /// Marks the Entity as spa.entityStates.removed (or spa.entityStates.junk if currently added)
        /// </summary>
        if (self.entityState() == spa.entityStates.added) {
            self.entityState(spa.entityStates.junk);
        }
        else if (self.entityState() != spa.entityStates.junk) {
            self.entityState(spa.entityStates.removed);
        }
    };

    self.hasChanges = ko.computed(function () {
        /// <summary>
        /// Determines if the item has changes 
        /// (and updates the entityState as necessary)
        /// </summary>
        /// <return>
        /// bool (if the item has changes)
        /// </return>
        for (var prop in self.origData) {
            if (self.origData[prop] != ko.utils.unwrapObservable(self.entity[prop])) {
                self.setModified();
                return true;
            }
        }
        self.setUnchanged();
        return false;
    });


    self.toJSON = function () {
        /// <summary>
        /// Return the Entity as Json (for submission to server)
        /// </summary>
        return ko.mapping.toJSON(self.entity);
    };

    return {
        // Standard Items
        EntityState: self.getState,
        HasChanges: self.hasChanges,
        JsonData: self.toJSON,
        DataRefresh: self.dataRefresh,
        MarkForDelete: self.markForDeletion,
        Key: self.key, 

        // Entity Properties
        ID: self.entity.ID,
        Name: self.entity.Name,
        Price: self.entity.Price,
        Quantity: self.entity.Quantity,
        Category: self.entity.Category,
    };
};

spa.viewModel = function () {
    /// <summary>
    /// The application View Model
    /// &#10; The Root binding Object
    /// </summary>
    var self = this;
    self.items = ko.observableArray();
    self.selectedItem = ko.observable();
    self.data = spa.dataContext("Products", spa.itemModel);

    // Perform Initial Load

    self.loadDataCallback = function (entities) {
        /// <summary>
        /// Loads the Data returned from the DataContext Object
        /// &#10; Creates items and fills the items array
        /// </summary>
        self.items.removeAll();
        $.map(entities, function (item) {
            self.items.push(item);
        });
    };

    self.data.LoadAll(self.loadDataCallback);

    self.addItem = function () {
        /// <summary>
        /// Adds a new Item (not sent to the server)
        /// </summary>
        var newItem = new spa.itemModel({ ID: 0, Name: "<New Item>", Price: 0, Quantity: 0, Category: "" }, spa.entityStates.added);
        self.data.Attach(newItem);
        self.items.push(newItem);
        self.selectedItem(newItem);
    };

    self.deleteItem = function (toDelete) {
        /// <summary>
        /// Marks an Item for Deletion
        /// </summary>
        toDelete.MarkForDelete();
        self.items.remove(toDelete);
        if (self.selectedItem() == toDelete)
            self.selectedItem(null);
    };

    return {
        Items: self.items,
        SelectedItem: self.selectedItem,
        AddItem: self.addItem,
        Save: self.data.SaveAll,
        DeleteItem: self.deleteItem,
    }
};

$(document).ready(function () {
    ko.applyBindings(new spa.viewModel());
});

